function _encodeState(prev1, prev2, ppair, psets, joker) {
	return prev1 | (prev2 << 4) | (ppair << 8) | (psets << 9) | (joker << 12)
}

function _calcNextStates(states, total, mx, banChow, banPung, banPair) {
	let nextStates = {}
	for (const state in states) {
		let prev1 = (state & 15)
		let prev2 = ((state >> 4) & 15)
		if (banChow && prev2)
			continue
		let prev = prev1 + prev2
		let ppair = ((state >> 8) & 1)
		let psets = ((state >> 9) & 7)
		let joker = (state >> 12)

		for (let count = (total + joker < mx ? total + joker : mx) - prev; count >= 0; --count) {
			let nextJoker = (count + prev > total) ? (total + joker - (count + prev)) : joker

			// No pair or pung
			if (count <= psets)
				nextStates[_encodeState(count, prev1, ppair, psets - count, nextJoker)] = true

			if (count >= 2 && count - 2 <= psets) {
				// Pair
				if (ppair && !banPair)
					nextStates[_encodeState(count - 2, prev1, 0, psets - count + 2, nextJoker)] = true

				// Pung
				if (count >= 3 && !banPung)
					nextStates[_encodeState(count - 3, prev1, ppair, psets - count + 2, nextJoker)] = true
			}
		}
	}
	return nextStates
}

function _endSuit(states) {
	let nextStates = {}
	for (const state in states) {
		if (!(state & 255))
			nextStates[state] = true
	}
	return nextStates
}

function _isHu(hand, joker = 0, banSuit = 0, banRank = 0, requirements = []) {
	let counter = newTileCounter()
	let table = new Array(39).fill(0)
	let sets = []

	// Add tiles, ignoring additional information
	for (const tile of hand.tiles) {
		++table[getTileValue(tile)]
	}

	// Add melds
	countTileMelds(counter, hand.melds)
	for (const meld of hand.melds) {
		sets.push(meld[1])
	}
	let requiredSets = 4 - sets.length, requiredPairs = 1

	// Check requirements
	for (const requirement of requirements) {
		{
			const index = sets.indexOf(requirement)
			if (index !== -1) {
				sets[index] = sets[sets.length - 1]
				sets.pop()
				continue
			}
		}

		if (requirement < 40) {
			// Pair
			if ((--requiredPairs) < 0) {
				return false
			}
			if ((counter[requirement] -= 2) < 0) {
				return false
			}
			if (table[requirement] < 2) {
				if (joker < 2 - table[requirement]) {
					return false
				}
				joker -= 2 - table[requirement]
				table[requirement] = 0
			} else {
				table[requirement] -= 2
			}
		} else {
			// Set
			if ((--requiredSets) < 0) {
				return false
			}
			if (requirement < 80) {
				// Pung
				const r = requirement - 40
				if ((counter[r] -= 3) < 0) {
					return false
				}
				if (table[r] < 3) {
					if (joker < 3 - table[r]) {
						return false
					}
					joker -= 3 - table[r]
					table[r] = 0
				} else {
					table[r] -= 3
				}
			} else {
				// Chow
				for (let r = requirement - 80; r <= requirement - 72; r += 4) {
					if ((--counter[r]) < 0) {
						return false
					}
					if (table[r] < 1) {
						if (joker < 1 - table[r]) {
							return false
						}
						joker -= 1 - table[r]
						table[r] = 0
					} else {
						--table[r]
					}
				}
			}
		}
	}

	// Check sets
	for (const set of sets) {
		if (banSuit & (1 << (set & 3)) || banRank[set]) {
			return false
		}
	}

	// Check rest tiles by dynamic programming
	let states = {}
	states[_encodeState(0, 0, requiredPairs, requiredSets, joker)] = true

	for (let suit = 0; suit < 4; ++suit)if (!(banSuit & (1 << suit))) {
		for (let tile = (suit === 3 ? 31 : (36 | suit)); tile > 3; tile -= 4) {
			states = _calcNextStates(states, table[tile], counter[tile], banRank[tile + 80] || (suit === 3), banRank[tile + 40], banRank[tile])
		}
		states = _endSuit(states)
	}

	for (let j = 0; j <= joker; ++j) {
		if (states[_encodeState(0, 0, 0, 0, j)]) {
			return true
		}
	}
	return false
}